#include "ShaderProgram.h"
#include <fstream>
using std::ifstream;
using std::ios;

#include <sstream>
#include <sys/stat.h>

namespace pancake
{
	struct ShaderFileExtension
	{
		const char* ext;
		GLShaderType type;
	};

	struct ShaderFileExtension extensions[] =
	{
		{".vs", GLShaderType::VERTEX},
		{".vert", GLShaderType::VERTEX},
		{".gs", GLShaderType::GEOMETRY},
		{".geom", GLShaderType::GEOMETRY},
		{".tcs", GLShaderType::TESS_CONTROL},
		{".tes", GLShaderType::TESS_EVALUATION},
		{".fs", GLShaderType::FRAGMENT},
		{".frag", GLShaderType::FRAGMENT},
		{".cs", GLShaderType::COMPUTE} 
	};

	ShaderProgram::ShaderProgram() : program_ID_(0), linked_(false) {}

	ShaderProgram::~ShaderProgram()
	{
		//if there is no program handle 
		//there is nothing to remove
		if(program_ID_ == 0) return;
		

		//find the number of attached shaders
		GLint num_shaders= 0;
		glGetProgramiv(program_ID_, GL_ATTACHED_SHADERS, &num_shaders);

		//get the shader names
		GLuint* shaders= new GLuint[num_shaders];
		glGetAttachedShaders(program_ID_, num_shaders, NULL, shaders);

		//delete shaders
		for(int i= 0; i< num_shaders; i++)
			glDeleteShader(shaders[i]);

		//delete the program
		glDeleteProgram(program_ID_);

		delete[] shaders;
	}

	void ShaderProgram::CreateProgram()
	{
		program_ID_= glCreateProgram();
	}
	
	void ShaderProgram::FreeProgram()
	{
		//delete the program
		glDeleteProgram(program_ID_);
	}

	string ShaderProgram::GetExtension(const char* name)
	{
		string name_string(name);

		size_t loc= name_string.find_last_of('.');
		if(loc != string::npos)
		{
			return name_string.substr(loc, string::npos);
		}

		return "";
	}

	bool ShaderProgram::FileExists(const string& file_name)
	{
		struct stat info;
		int ret= -1;

		ret= stat(file_name.c_str(), &info);
		return 0 == ret;
	}

	void ShaderProgram::LoadShader(const char* file_name) throw (GLProgramException)
	{
		//find the number of possible extensions a shader can have
		int num_exts= sizeof(extensions) / sizeof(ShaderFileExtension);

		//check the file name's extension to determine the shader type
		string extension= GetExtension(file_name);
		GLShaderType type= GLShaderType::VERTEX;
		bool match_found= false;
		for(int i= 0; i < num_exts; i++)
		{
			if(extension == extensions[i].ext)
			{
				match_found= true;
				type= extensions[i].type;
				break;
			}
		}

		//if there wasn't a match, throw an exception
		if(!match_found)
		{
			string message= "Unrecognized extension: " + extension;
			throw GLProgramException(message);
		}

		//pass the discovered shader type along
		LoadShader(file_name, type);
	}

	void ShaderProgram::LoadShader(const char* file_name, GLShaderType type) throw (GLProgramException)
	{
		if(!FileExists(file_name))
		{
			string message= string("Shader: ") + file_name + " not found.";
			throw GLProgramException(message);
		}

		if(program_ID_ <= 0)
		{
			program_ID_= glCreateProgram();
			if(program_ID_ == 0)
			{
				throw GLProgramException("Unable to create shader program.");
			}
		}

		ifstream in_file(file_name, ios::in);
		if(!in_file)
		{
			string message= string("Unable to open: ") + file_name;
			throw GLProgramException(message);
		}

		//get file contents
		std::stringstream code;
		code<<in_file.rdbuf();
		in_file.close();

		LoadShader(code.str(), type, file_name);
	}

	void ShaderProgram::LoadShader(const string& source, GLShaderType type, const char* file_name) throw (GLProgramException)
	{
		if(program_ID_ <= 0)
		{
			program_ID_ = glCreateProgram();
			if(program_ID_ == 0)
			{
				throw GLProgramException("Unable to create shader program.");
			}
		}

		GLuint shader_handle= glCreateShader(type);

		const char* c_code= source.c_str();
		glShaderSource(shader_handle, 1, &c_code, NULL);

		//compile the shader
		glCompileShader(shader_handle);

		//check for errors
		int result;
		glGetShaderiv(shader_handle, GL_COMPILE_STATUS, &result);
		if(GL_FALSE == result)
		{
			string log_string="";
			log_string= GetShaderLog(shader_handle);

			string message;
			if(file_name)
			{
				message= string(file_name) + ": shader compilation failed.\n";
			}
			else
			{
				message= "Shader compilation failed.\n";
			}
			message+= log_string;

			throw GLProgramException(message);
		}
		else
		{
			glAttachShader(program_ID_, shader_handle);
		}
	}

	//binds the program for use
	bool ShaderProgram::Bind() throw (GLProgramException)
	{
		if(program_ID_ <= 0 || (!linked_))
			throw GLProgramException("Shader has not been linked");
		//use the program 
		glUseProgram(program_ID_);
	}

	//unbinds the class's program
	void ShaderProgram::UnBind()
	{
		//use the default program
		glUseProgram(NULL);
	}

	void ShaderProgram::Link() throw(GLProgramException)
	{
		if(linked_) return;
		if(program_ID_ <= 0)
			throw GLProgramException("Program has not been compiled.");

		glLinkProgram(program_ID_);

		int status= 0;
		glGetProgramiv(program_ID_, GL_LINK_STATUS, &status);
		if(status == GL_FALSE)
		{
			//store log and return false
			string log_string;
			log_string= GetProgramLog();
			throw GLProgramException(string("Program link failed:\n") + log_string); 
		}
		else
		{
			uniform_locations_.clear();
			linked_= true;
		}
	}

	//returns if program is linked
	bool ShaderProgram::IsLinked()
	{
		return linked_;
	}

	//prints a log of the program
	string ShaderProgram::GetProgramLog()
	{
		string message= "";
		//make sure the program has been loaded with program handle
		if(glIsProgram(program_ID_))
		{
			//program log length
			int info_log_length= 0;
			int max_length= info_log_length;

			//get info string length
			glGetProgramiv(program_ID_, GL_INFO_LOG_LENGTH, &max_length);

			if(max_length > 0)
			{
				//allocate string
				char* info_log= new char[max_length];

				//get info log
				glGetProgramInfoLog(program_ID_, max_length, &info_log_length, info_log);
				//print log
				message= info_log;

				//deallocate string
				delete[] info_log;
			}
		}
		else
		{
			message= "NO INFO LOG: Program has not been created";
		}
		return message;
	}

	//prints an information log of the shader
	string ShaderProgram::GetShaderLog(GLuint shader)
	{
		string message= "";
		//make sure the handle passed is a valid shader
		if(glIsShader(shader))
		{
			//shader log length
			int info_log_length= 0;
			int max_length= info_log_length;

			//get info string length
			glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &max_length);

			if(max_length > 0)
			{
				//allocate string
				char* info_log= new char[max_length];

				//get info log
				glGetShaderInfoLog(shader, max_length, &info_log_length, info_log);
				//print log
				message= info_log;

				//deallocate string
				delete[] info_log;
			}
		}
		else
		{
			message= "Shader doesn't exist.";
		}
		return message;
	}

	int ShaderProgram::GetUniformLocation(const char* name)
	{
		std::map<string, int>::iterator pos;
		pos= uniform_locations_.find(name);

		if(pos == uniform_locations_.end())
		{
			uniform_locations_[name]= glGetUniformLocation(program_ID_, name);
		}

		return uniform_locations_[name];
	}

	void ShaderProgram::BindAttribLocation( GLuint location, const char * name)
	{
		glBindAttribLocation(program_ID_, location, name);
	}
	
	void ShaderProgram::BindFragDataLocation( GLuint location, const char * name )
	{
		glBindFragDataLocation(program_ID_, location, name);
	}

	void ShaderProgram::SetUniform( const char *name, float x, float y, float z)
	{
		GLint loc= GetUniformLocation(name);
		glUniform3f(loc, x, y, z);
	}
	
	void ShaderProgram::SetUniform( const char *name, const vec2 & v)
	{
		GLint loc= GetUniformLocation(name);
		glUniform2f(loc, v.x, v.y);
	}
	
	void ShaderProgram::SetUniform( const char *name, const vec3 & v)
	{
		this->SetUniform(name, v.x, v.y, v.z);
	}
	
	void ShaderProgram::SetUniform( const char *name, const vec4 & v)
	{
		GLint loc= GetUniformLocation(name);
		glUniform4f(loc, v.x, v.y, v.z, v.w);
	}

	void ShaderProgram::SetUniform( const char *name, const mat4 & m)
	{
		GLint loc= GetUniformLocation(name);
		glUniformMatrix4fv(loc, 1, GL_FALSE, &m[0][0]);
	}

	void ShaderProgram::SetUniform( const char *name, const mat3 & m)
	{
		GLint loc= GetUniformLocation(name);
		glUniformMatrix3fv(loc, 1, GL_FALSE, &m[0][0]);
	}

	void ShaderProgram::SetUniform( const char *name, float val)
	{
		GLint loc= GetUniformLocation(name);
		glUniform1f(loc, val);
	}
	
	void ShaderProgram::SetUniform( const char *name, int val)
	{
		GLint loc= GetUniformLocation(name);
		glUniform1i(loc, val);
	}
	void ShaderProgram::SetUniform( const char *name, bool val)
	{
		GLint loc= GetUniformLocation(name);
		glUniform1i(loc, val);
	}
	void ShaderProgram::SetUniform( const char *name, GLuint val)
	{
		GLint loc= GetUniformLocation(name);
		glUniform1ui(loc, val);
	}

	void ShaderProgram::PrintActiveUniforms()
	{
		GLint num_uniforms= 0;
		glGetProgramInterfaceiv(program_ID_, GL_UNIFORM, GL_ACTIVE_RESOURCES, &num_uniforms);

		GLenum properties[]= {GL_NAME_LENGTH, GL_TYPE, GL_LOCATION, GL_BLOCK_INDEX};
		printf("Active uniforms:\n");
		for(int i= 0; i<num_uniforms; i++)
		{
			GLint results[4];
			glGetProgramResourceiv(program_ID_, GL_UNIFORM, i, 4, properties, 4, NULL, results);

			if(results[3] != -1) continue;
			GLint name_buff_size= results[0] + 1;
			char* name= new char[name_buff_size];
			glGetProgramResourceName(program_ID_, GL_UNIFORM, i, name_buff_size, NULL, name);
			printf("%-5d %s (%s)\n", results[2], name, GetTypeString(results[1]));
			delete[] name;
		}
	}

	void ShaderProgram::PrintActiveUniformBlocks()
	{
		GLint num_blocks= 0;

		glGetProgramInterfaceiv(program_ID_, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES, &num_blocks);
		GLenum block_properties[]= {GL_NUM_ACTIVE_VARIABLES, GL_NAME_LENGTH};
		GLenum block_index[]= {GL_ACTIVE_VARIABLES};
		GLenum properties[]= {GL_NAME_LENGTH, GL_TYPE, GL_BLOCK_INDEX};

		for(int block= 0; block< num_blocks; block++)
		{
			GLint block_info[2];
			glGetProgramResourceiv(program_ID_, GL_UNIFORM_BLOCK, block, 2, block_properties, 2, NULL, block_info);
			GLint num_unis= block_info[0];

			char* block_name= new char[block_info[1]+1];
			glGetProgramResourceName(program_ID_, GL_UNIFORM_BLOCK, block, block_info[1]+1, NULL, block_name);
			printf("Uniform block \"%s\":\n", block_name);
			delete[] block_name;

			GLint* unif_indices= new GLint[num_unis];
			glGetProgramResourceiv(program_ID_, GL_UNIFORM_BLOCK, block, 1, block_index, num_unis, NULL, unif_indices);

			for(int unif= 0; unif < num_unis; unif++)
			{
				GLint uni_index= unif_indices[unif];
				GLint results[3];
				glGetProgramResourceiv(program_ID_, GL_UNIFORM, uni_index, 3, properties, 3, NULL, results);

				GLint name_buff_size= results[0] + 1;
				char* name= new char[name_buff_size];
				glGetProgramResourceName(program_ID_, GL_UNIFORM, uni_index, name_buff_size, NULL, name);
				printf("%s (%s)\n", name, GetTypeString(results[1]));
				delete[] name;
			}

			delete[] unif_indices;
		}
	}

	void ShaderProgram::PrintActiveAttribs()
	{
		GLint num_attribs;
		glGetProgramInterfaceiv(program_ID_, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES, &num_attribs);

		GLenum properties[]= {GL_NAME_LENGTH, GL_TYPE, GL_LOCATION};

		printf("Active attributes:\n");
		for(int i= 0; i < num_attribs; i++)
		{
			GLint results[3];
			glGetProgramResourceiv(program_ID_, GL_PROGRAM_INPUT, i, 3, properties, 3, NULL, results);

			GLint name_buff_size= results[0] + 1;
			char* name= new char[name_buff_size];
			glGetProgramResourceName(program_ID_, GL_PROGRAM_INPUT, i, name_buff_size, NULL, name);
			printf("%-5d %s (%s)\n", results[2], name, GetTypeString(results[1]));
			delete[] name;
		}
	}

	void ShaderProgram::Validate() throw(GLProgramException)
	{
		if(!IsLinked())
			throw GLProgramException("Program is not linked");

		GLint status;
		glValidateProgram(program_ID_);
		glGetProgramiv(program_ID_, GL_VALIDATE_STATUS, &status);

		if(status == GL_FALSE)
		{
			//store log and return false
			string log_string= GetProgramLog();
			throw GLProgramException(string("Program failed to validate\n") + log_string);
		}
	}

	const char* ShaderProgram::GetTypeString(GLenum type)
	{
		switch(type)
		{
		case GL_FLOAT:
			return "float";
		case GL_FLOAT_VEC2:
			return "vec2";
		case GL_FLOAT_VEC3:
			return "vec3";
		case GL_FLOAT_VEC4:
			return "vec4";
		case GL_DOUBLE:
			return "double";
		case GL_INT:
			return "int";
		case GL_UNSIGNED_INT:
			return "unsigned int";
		case GL_BOOL:
			return "bool";
		case GL_FLOAT_MAT2:
			return "mat2";
		case GL_FLOAT_MAT3:
			return "mat3";
		case GL_FLOAT_MAT4:
			return "mat4";
		default:
			return "?";
		}
	}
}; //namespace